﻿//-----------------------------------------------------------------------
// <copyright file="GoatCodeGenerator.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace GoatGen.Generator
{
    using System;
    using System.CodeDom;
    using System.IO;
    using GoatGen.Object;
    using Microsoft.CSharp;

    /// <summary>
    /// Class which generates C# code from the provided Goat object.
    /// </summary>
    public partial class GoatCodeGenerator
    {
        #region Fields - Private

        /// <summary>
        /// Goat object we are build code from.
        /// </summary>
        private GoatObject obj;

        /// <summary>
        /// Code compilation unit we are using.
        /// </summary>
        private CodeCompileUnit compileUnit;

        #endregion

        #region Constructor

        /// <summary>
        /// Constructor for the generator.
        /// </summary>
        /// <param name="obj">Goat object to build code from.</param>
        public GoatCodeGenerator(GoatObject obj)
        {
            this.obj = obj;
            this.compileUnit = new CodeCompileUnit();
        }

        #endregion

        #region Methods - Public

        /// <summary>
        /// Generate C# code and output it to the passed text writer.
        /// </summary>
        /// <param name="writer">Text writer to output code to.</param>
        public void Generate(TextWriter writer)
        {
            if (writer == null)
            {
                throw new ArgumentNullException("writer", "Writer must be non-null.");
            }

            try
            {
                CodeNamespace codeNamespace = this.AddNamespace();
                CodeTypeDeclaration type = this.AddObjectDeclaration(codeNamespace);

                this.AddFields(type);
                this.AddConstructor(type);
                this.AddProperties(type);

                CSharpCodeProvider provider = new CSharpCodeProvider();

                provider.GenerateCodeFromCompileUnit(this.compileUnit, writer, null);
            }
            catch (Exception e)
            {
                throw new GoatException("Failed to generate code: " + e.ToString(), e);
            }
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Add properties to the passed type declaration.
        /// </summary>
        /// <param name="type">Type to add properties to.</param>
        private void AddProperties(CodeTypeDeclaration type)
        {
            foreach (GoatField field in this.obj.Fields)
            {
                CodeMemberProperty property = new CodeMemberProperty();

                property.Name = field.Name;
                property.Type = new CodeTypeReference(field.Type);
                property.Attributes = MemberAttributes.Public | MemberAttributes.Final;

                dynamic typedField = field;

                this.HandleAddSetProperty(typedField, property);
                this.HandleAddGetProperty(typedField, property);

                type.Members.Add(property);
            }
        }

        /// <summary>
        /// Handle adding a 'set' property to the passed field.
        /// </summary>
        /// <param name="field">Field to add a set property for.</param>
        /// <param name="property">Property we are operating on.</param>
        private void HandleAddSetProperty(GoatField field, CodeMemberProperty property)
        {
        }

        /// <summary>
        /// Handle adding a 'get' property to the passed field.
        /// </summary>
        /// <param name="field">Field to add a get property for.</param>
        /// <param name="property">Property we are operating on.</param>
        private void HandleAddGetProperty(GoatField field, CodeMemberProperty property)
        {
            CodeFieldReferenceExpression fieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), this.GenerateFieldName(field));
            CodeMethodReturnStatement returnStatement = new CodeMethodReturnStatement(fieldRef);

            property.GetStatements.Add(returnStatement);
        }

        /// <summary>
        /// Utility function to add a 'set' for an array field.
        /// </summary>
        /// <param name="field">Field we are adding a set for.</param>
        /// <param name="property">Property we are operating on.</param>
        /// <param name="size">Size of the array we are expecting.</param>
        private void AddPropertySetArray(GoatField field, CodeMemberProperty property, int size)
        {
            CodePropertySetValueReferenceExpression valueRef = new CodePropertySetValueReferenceExpression();

            // Check for a null value
            CodeBinaryOperatorExpression nullCompare = new CodeBinaryOperatorExpression(
                valueRef,
                CodeBinaryOperatorType.IdentityEquality,
                new CodePrimitiveExpression(null));
            CodeConditionStatement nullConditional = new CodeConditionStatement(nullCompare, new CodeMethodReturnStatement());

            property.SetStatements.Add(nullConditional);

            // Make sure length is correct
            // value.Length
            CodePropertyReferenceExpression valueLengthRef = new CodePropertyReferenceExpression(valueRef, "Length");

            // value.Length != size
            CodeBinaryOperatorExpression lengthCompare = new CodeBinaryOperatorExpression(
                valueLengthRef,
                CodeBinaryOperatorType.IdentityInequality,
                new CodePrimitiveExpression(size));

            // if (value.Length != size) { return; }
            CodeConditionStatement lengthConditional = new CodeConditionStatement(lengthCompare, new CodeMethodReturnStatement());

            property.SetStatements.Add(lengthConditional);

            // this.fieldName[i] = value[i];
            string fieldName = this.GenerateFieldName(field);
            CodeFieldReferenceExpression fieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fieldName);

            for (int i = 0; i < size; ++i)
            {
                CodeArrayIndexerExpression valueIndexExpression = new CodeArrayIndexerExpression(valueRef, new CodePrimitiveExpression(i));
                CodeArrayIndexerExpression thisIndexExpression = new CodeArrayIndexerExpression(fieldRef, new CodePrimitiveExpression(i));
                CodeAssignStatement assignment = new CodeAssignStatement(thisIndexExpression, valueIndexExpression);

                property.SetStatements.Add(assignment);
            }
        }

        /// <summary>
        /// Utility function to add a 'set' for an array field that is clamped.
        /// </summary>
        /// <typeparam name="T">Array type.</typeparam>
        /// <param name="field">Field we are adding a set for.</param>
        /// <param name="property">Property we are operating on.</param>
        /// <param name="size">Size of the array we are expecting.</param>
        /// <param name="minValid">Bool which tells if min is valid.</param>
        /// <param name="min">Min value. Only used if minValid is true.</param>
        /// <param name="maxValid">Bool which tells if max is valid.</param>
        /// <param name="max">Max value. Only used if maxValid is true.</param>
        private void AddPropertySetArrayClamped<T>(GoatField field, CodeMemberProperty property, int size, bool minValid, T[] min, bool maxValid, T[] max)
        {
            CodePropertySetValueReferenceExpression valueRef = new CodePropertySetValueReferenceExpression();

            // Check for a null value
            CodeBinaryOperatorExpression nullCompare = new CodeBinaryOperatorExpression(
                valueRef,
                CodeBinaryOperatorType.IdentityEquality,
                new CodePrimitiveExpression(null));
            CodeConditionStatement nullConditional = new CodeConditionStatement(nullCompare, new CodeMethodReturnStatement());

            property.SetStatements.Add(nullConditional);

            // Make sure length is correct
            // value.Length
            CodePropertyReferenceExpression valueLengthRef = new CodePropertyReferenceExpression(valueRef, "Length");

            // value.Length != size
            CodeBinaryOperatorExpression lengthCompare = new CodeBinaryOperatorExpression(
                valueLengthRef,
                CodeBinaryOperatorType.IdentityInequality,
                new CodePrimitiveExpression(size));

            // if (value.Length != size) { return; }
            CodeConditionStatement lengthConditional = new CodeConditionStatement(lengthCompare, new CodeMethodReturnStatement());

            property.SetStatements.Add(lengthConditional);

            // this.fieldName[i] = clampedValue;
            string fieldName = this.GenerateFieldName(field);
            CodeFieldReferenceExpression fieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fieldName);

            CodeVariableDeclarationStatement clampedValue = new CodeVariableDeclarationStatement(typeof(T), "clampedValue");
            
            property.SetStatements.Add(clampedValue);

            CodeVariableReferenceExpression clampedValueExpression = new CodeVariableReferenceExpression("clampedValue");

            for (int i = 0; i < size; ++i)
            {
                CodeArrayIndexerExpression valueIndexExpression = new CodeArrayIndexerExpression(valueRef, new CodePrimitiveExpression(i));

                CodeAssignStatement clampedValueAssign = new CodeAssignStatement(clampedValueExpression, valueIndexExpression);
                property.SetStatements.Add(clampedValueAssign);

                if (minValid)
                {
                    // if (value < min[i]) { clampedValue = min[i]; }
                    CodePrimitiveExpression minExpression = new CodePrimitiveExpression(min[i]);
                    CodeBinaryOperatorExpression minCompare = new CodeBinaryOperatorExpression(valueIndexExpression, CodeBinaryOperatorType.LessThan, minExpression);
                    CodeAssignStatement minAssign = new CodeAssignStatement(clampedValueExpression, minExpression);
                    CodeConditionStatement minConditional = new CodeConditionStatement(minCompare, minAssign);

                    property.SetStatements.Add(minConditional);
                }

                if (maxValid)
                {
                    // if (value > max[i]) { clampedValue = max[i]; }
                    CodePrimitiveExpression maxExpression = new CodePrimitiveExpression(min[i]);
                    CodeBinaryOperatorExpression maxCompare = new CodeBinaryOperatorExpression(valueIndexExpression, CodeBinaryOperatorType.GreaterThan, maxExpression);
                    CodeAssignStatement minAssign = new CodeAssignStatement(clampedValueExpression, maxExpression);
                    CodeConditionStatement maxConditional = new CodeConditionStatement(maxCompare, minAssign);

                    property.SetStatements.Add(maxConditional);
                }

                CodeArrayIndexerExpression thisIndexExpression = new CodeArrayIndexerExpression(fieldRef, new CodePrimitiveExpression(i));
                CodeAssignStatement assignment = new CodeAssignStatement(thisIndexExpression, clampedValueExpression);

                property.SetStatements.Add(assignment);
            }
        }

        /// <summary>
        /// Utility function to add a 'set' for a primitive field that is clamped.
        /// </summary>
        /// <param name="field">Field we are adding a set for.</param>
        /// <param name="property">Property we are operating on.</param>
        /// <param name="minValid">Bool which tells if min is valid.</param>
        /// <param name="min">Min value. Only used if minValid is true.</param>
        /// <param name="maxValid">Bool which tells if max is valid.</param>
        /// <param name="max">Max value. Only used if maxValid is true.</param>
        private void AddPropertySetPrimitiveClamped(GoatField field, CodeMemberProperty property, bool minValid, object min, bool maxValid, object max)
        {
            CodePropertySetValueReferenceExpression valueRef = new CodePropertySetValueReferenceExpression();

            if (minValid)
            {
                // if (value < min) { return; }
                CodeBinaryOperatorExpression minCompare = new CodeBinaryOperatorExpression(
                    valueRef,
                    CodeBinaryOperatorType.LessThan,
                    new CodePrimitiveExpression(min));
                CodeConditionStatement minConditional = new CodeConditionStatement(minCompare, new CodeMethodReturnStatement());

                property.SetStatements.Add(minConditional);
            }

            if (maxValid)
            {
                // if (value > max) { return; }
                CodeBinaryOperatorExpression maxCompare = new CodeBinaryOperatorExpression(
                    valueRef,
                    CodeBinaryOperatorType.GreaterThan,
                    new CodePrimitiveExpression(max));
                CodeConditionStatement maxConditional = new CodeConditionStatement(maxCompare, new CodeMethodReturnStatement());

                property.SetStatements.Add(maxConditional);
            }

            string fieldName = this.GenerateFieldName(field);
            CodeFieldReferenceExpression fieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fieldName);
            CodeAssignStatement assignment = new CodeAssignStatement(fieldRef, valueRef);

            property.SetStatements.Add(assignment);
        }

        /// <summary>
        /// Utility function to add a 'set' for an object field.
        /// </summary>
        /// <param name="field">Field we are adding a set for.</param>
        /// <param name="property">Property we are operating on.</param>
        /// <param name="allowNull">True if we will allow null values.</param>
        private void AddPropertySetObject(GoatField field, CodeMemberProperty property, bool allowNull)
        {
            CodePropertySetValueReferenceExpression valueRef = new CodePropertySetValueReferenceExpression();

            // Check for a null value
            if (!allowNull)
            {
                CodeBinaryOperatorExpression nullCompare = new CodeBinaryOperatorExpression(
                    valueRef,
                    CodeBinaryOperatorType.IdentityEquality,
                    new CodePrimitiveExpression(null));
                CodeConditionStatement nullConditional = new CodeConditionStatement(nullCompare, new CodeMethodReturnStatement());

                property.SetStatements.Add(nullConditional);
            }

            string fieldName = this.GenerateFieldName(field);
            CodeFieldReferenceExpression fieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fieldName);
            CodeAssignStatement assignment = new CodeAssignStatement(fieldRef, valueRef);

            property.SetStatements.Add(assignment);
        }

        /// <summary>
        /// Add a constructor to the passed type declaration. Will initialize all fields here as well.
        /// </summary>
        /// <param name="type">Type declaration to add the constructor to.</param>
        private void AddConstructor(CodeTypeDeclaration type)
        {
            CodeConstructor constructor = new CodeConstructor();

            constructor.Attributes = MemberAttributes.Public;

            foreach (GoatField field in this.obj.Fields)
            {
                dynamic typedField = field;

                this.HandleFieldInitialization(typedField, constructor);
            }

            type.Members.Add(constructor);
        }

        /// <summary>
        /// Called to handle field initialization. In this case we have an unknown field
        /// since no method was found for the current type.
        /// </summary>
        /// <param name="field">Field to initialize.</param>
        /// <param name="constructor">Constructor we are populating.</param>
        private void HandleFieldInitialization(GoatField field, CodeConstructor constructor)
        {
            throw new GoatException("Unknown field type: " + field.GetType());
        }

        /// <summary>
        /// Add a field initialization statement.
        /// </summary>
        /// <param name="field">Field we are initializing.</param>
        /// <param name="constructor">Constructor we are adding to.</param>
        /// <param name="defaultValue">Default field value.</param>
        private void AddFieldInitialization(GoatField field, CodeConstructor constructor, object defaultValue)
        {
            CodeVariableReferenceExpression varRef = new CodeVariableReferenceExpression(this.GenerateFieldName(field));
            CodePrimitiveExpression expression = new CodePrimitiveExpression(defaultValue);
            CodeAssignStatement assignment = new CodeAssignStatement(varRef, expression);

            constructor.Statements.Add(assignment);
        }

        /// <summary>
        /// Add a field initialization statement for an array.
        /// </summary>
        /// <typeparam name="T">Array type.</typeparam>
        /// <param name="field">Field we are initializing.</param>
        /// <param name="constructor">Constructor we are adding to.</param>
        /// <param name="values">Default array values.</param>
        private void AddFieldInitializationArray<T>(GoatField field, CodeConstructor constructor, T[] values)
        {
            CodeArrayCreateExpression arrayCreate = new CodeArrayCreateExpression(field.Type, values.Length);

            foreach (T component in values)
            {
                arrayCreate.Initializers.Add(new CodePrimitiveExpression(component));
            }

            CodeVariableReferenceExpression varRef = new CodeVariableReferenceExpression(this.GenerateFieldName(field));
            CodeAssignStatement assignment = new CodeAssignStatement(varRef, arrayCreate);

            constructor.Statements.Add(assignment);
        }

        /// <summary>
        /// Add all fields to the passed declaration.
        /// </summary>
        /// <param name="type">Declaration we are adding fields to.</param>
        private void AddFields(CodeTypeDeclaration type)
        {
            foreach (GoatField field in this.obj.Fields)
            {
                string fieldName = this.GenerateFieldName(field);

                CodeMemberField memberField = new CodeMemberField(field.Type, fieldName);

                memberField.Attributes = MemberAttributes.Private;

                type.Members.Add(memberField);
            }
        }

        /// <summary>
        /// Add an object declaration.
        /// </summary>
        /// <param name="codeNamespace">Namespace to add the declaration to.</param>
        /// <returns>Returns the declaration for the object that was created.</returns>
        private CodeTypeDeclaration AddObjectDeclaration(CodeNamespace codeNamespace)
        {
            CodeTypeDeclaration type = new CodeTypeDeclaration(this.obj.Name);
            
            type.Attributes = MemberAttributes.Public;

            codeNamespace.Types.Add(type);

            return type;
        }

        /// <summary>
        /// Add a namespace to contain our generated object.
        /// </summary>
        /// <returns>Returns the created namespace.</returns>
        private CodeNamespace AddNamespace()
        {
            GoatFieldConfig config = this.obj.GetField(GoatFieldConstants.ConfigFieldName) as GoatFieldConfig;
            if (config == null)
            {
                throw new GoatException("Unable to find config field.");
            }

            CodeNamespace codeNamespace = new CodeNamespace(config.Namespace);

            this.compileUnit.Namespaces.Add(codeNamespace);

            return codeNamespace;
        }

        /// <summary>
        /// Given a field, generate a code field name for it.
        /// </summary>
        /// <param name="field">Field name to generate a name for.</param>
        /// <returns>Returns the code name of the field.</returns>
        private string GenerateFieldName(GoatField field)
        {
            return string.Format("_{0}", field.Name);
        }

        #endregion
    }
}
